<?php
/**
 * Eka Framework 0.3.0
 *
 * @copyright Copyright (c) 2008 Alexey Yu. Chuprina <656b61@gmail.com> (http://php-eka.googlecode.com)
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 *
 * @version $Id$
 */

/**
 * Eka_Db_ModelBulder
 *
 * @package Db
 * @subpackage ModelBulder
 */
class Eka_Db_ModelBulder
{
	/**
	 * @var array
	 */
	protected $adapters = array();
	
	protected $adapterManager;
	
	public function __construct($adapterManager, array $adapterNames)
	{
		$this->adapterManager = $adapterManager;
		if (is_array($this->adapterManager)) {
			$adapterManager = call_user_func($this->adapterManager);
		}
		
		foreach ($adapterNames as $name) {
			$adapter = call_user_func(array($adapterManager, 'getDbAdapter'), $name);
			if (!$adapter instanceof Eka_Db_Adapter_Abstract) {
				require_once 'Eka/Db/ModelBulder/Exception.php';
				throw new Eka_Db_ModelBulder_Exception("Adapter manager not return object of Eka_Db_Adapter_Abstract by name '$name'");
			}
			$this->adapters[$name] = $adapter;
		}
	}
	
	public function build($modelPath, array $adapters)
	{
		
		$modelPath = realpath($modelPath);
		if (!$modelPath) {
			require_once 'Eka/Db/ModelBulder/Exception.php';
			throw new Eka_Db_ModelBulder_Exception("Directory '$modelPath' not found");
		}
		if (!is_writable($modelPath)) {
			require_once 'Eka/Db/ModelBulder/Exception.php';
			throw new Eka_Db_ModelBulder_Exception("Directory '$modelPath' not writable");
		}
		
		$modelPath .= DIRECTORY_SEPARATOR;
		$basePath = $modelPath . 'Base' . DIRECTORY_SEPARATOR;
		
		if (!is_dir($basePath)) {
			mkdir($basePath, 0755);
			chmod($basePath, 0755);
		}

		###

		$models = array();
		$this->tables = array();
		foreach ($adapters as $adapterName => $schemas) {
			$adapter = $this->adapters[$adapterName];
			foreach ($schemas as $schema) {
				$qSchema = $adapter->quoteIdentifier($schema);
				foreach ($adapter->fetchColumn("SHOW TABLES FROM $qSchema") as $tableName) {
					$model = $this->tableToModel($tableName);
					if (isset($models[$model])) {
						include_once 'Eka/Db/ModelBulder/Exception.php';
						throw new Eka_Db_ModelBulder_Exception("Model name '$model' already exist");
					}
					$models[$model] = array(
						'name' => $model,
						'adapter' => $adapterName,
						'table' => new Eka_Db_Table($adapter, $tableName, $schema),
						'metadata' => $adapter->describeTable($tableName, $schema)
					);
					$this->tables[$schema . '.' . $tableName] = $models[$model];
				}
			}
		}
		
		###
		
		$adapterTemplate = is_array($this->adapterManager)
			? $this->adapterManager[0] . '::' . $this->adapterManager[1] . '()->getDbAdapter(\'%adapter%\')'
			: $this->adapterManager . '::getDbAdapter(\'%adapter%\')';
		
		$lastAdapter = null;
		$lastSchema = null;
		foreach ($models as $model => $data) {
			$table = $data['table']->getName();
			$schema = $data['table']->getSchema();
			if ($data['adapter'] != $lastAdapter) {
				$lastAdapter = $data['adapter'];
				$this->printLine($lastAdapter);
			}
			if ($schema != $lastSchema) {
				$lastSchema = $schema;
				$this->printLine("\t$lastSchema");
			}
			$this->printLine("\t\t$table -> $model");
			
			$params = array(
				'%adapter%' => str_replace('%adapter%', $data['adapter'], $adapterTemplate),
				'%schema%' => $data['table']->getSchema(),
				'%table%' => $data['table']->getName(),
				'%model%' => $model,
				'%base_model%' => 'Base_' . $model,
				'%base_model_file%' => 'Base/' . $model . '.php',
				'%manager%' => $model . 'Manager',
				'%base_manager%' => 'Base_' . $model . 'Manager',
				'%base_manager_file%' => 'Base/' . $model . 'Manager.php',
				);

			
			$managerParams = array_merge($params, array(
				'%columns%' => $this->paramManagerColumns($data),
				'%primary%' => $this->paramManagerPrimary($data),
				'%methods%' => $this->paramManagerMethods($data)
				));
				
				
			$modelParams = array_merge($params, array(
				'%constants%' => $this->paramModelConstants($data),
				'%methods%' => $this->paramModelMethods($data)
				));
			
			file_put_contents($basePath . $model . '.php', strtr($this->templates['base-model-file'],  $modelParams));
			file_put_contents($basePath . $model . 'Manager.php', strtr($this->templates['base-manager-file'],  $managerParams));
		
			$file = $modelPath . $model . '.php';
			if (!file_exists($file)) {
				file_put_contents($file, strtr($this->templates['model-file'], $params));
			}
			$file = $modelPath . $model . 'Manager.php';
			if (!file_exists($file)) {
				file_put_contents($file, strtr($this->templates['manager-file'], $params));
			}
		}
		
		$this->tables = null;
	}
	
	###
	
	protected function paramModelMethods($data)
	{
		$result = array();
		
		$__get = array(
			'name' => '__get',
			'params' => array(array('name' => 'name', 'type'=> 'string')),
			'return' => array('type' => 'mixed'),
			'body' => "\t\$method = 'get' . self::columnToMethodName(\$name);\n"
				. "\t\tif (method_exists(\$this, \$method)) {\n"
				. "\t\t\treturn \$this->\$method();\n"
				. "\t\t}\n"
				. "\t\treturn parent::__get(\$name);"
		);
		
		$__set = array(
			'name' => '__set',
			'params' => array(array('name' => 'name', 'type'=> 'string'), array('name' => 'value', 'type'=> 'mixed')),
			'body' => "\t\$method = 'set' . self::columnToMethodName(\$name);\n"
				. "\t\tif (method_exists(\$this, \$method)) {\n"
				. "\t\t\t\$this->\$method(\$value);\n"
				. "\t\t\treturn;\n"
				. "\t\t}\n"
				. "\t\tparent::__set(\$name, \$value);"
		);
		
		$result[] = $this->createMethod($__get);
		$result[] = $this->createMethod($__set);
		
		$columns = $data['metadata']['columns'];
		foreach ($columns as $column) {
			$name = $this->formatName($column['name']);
			$get = array('name' => 'get' . $name);
			$set = array('name' => 'set' . $name);
			
			if ($column['rtype'] == 'date') {
				$get['return'] = array('type' => 'string');
				$set['params'] = array(array('name' => 'value', 'type'=> 'string'));
				$get['body'] = "\treturn parent::__get('".$column['name']."');";
				$set['body'] = "\treturn parent::__set('".$column['name']."', \$value);";
			} else {
				$type = $this->dbTypeToPhpType($column['type'], $column['rtype']);
				$get['return'] = array('type' => $type);
				$set['params'] = array(array('name' => 'value', 'type'=> $type));
				$get['body'] = "\t\$v = parent::__get('".$column['name']."');\n"
					. "\t\treturn \$v === null ? null : ($type)\$v;";
				$set['body'] = "\treturn parent::__set('".$column['name']."', \$value);";
			}
			
			$result[] = $this->createMethod($get);
			$result[] = $this->createMethod($set);
		}

		$foreignKeys = $data['metadata']['foreign_keys'];
		foreach ($foreignKeys as $keyName => $foreign) {
			$fData = $this->tables[$foreign['schema'] . '.' . $foreign['table']];
			$name = $fData['name'];
			foreach ($foreignKeys as $sKeyName => $sForeign) {
				if ($sKeyName == $keyName) continue;
				if ($foreign['schema'].'.'.$foreign['table'] == $sForeign['schema'].'.'.$sForeign['table']) {
					$sufix = preg_replace('/^(.+)\-([^\-]+)$/', '\\2', $keyName);
					if ($sufix != $foreign['table']) {
						$name = $this->formatName($sufix) . $name;
					}
				}
			}
			
			$method = array(
				'name' => 'get' . $name,
				'return' => array('type' => $fData['name'])
			);
			$temp = array();
			foreach ($foreign['foreign'] as $column) {
				$temp[] = "parent::__get('$column')";
			}
			
			$method['body'] = "\treturn " . $fData['name'] . 'Manager::get('.join(', ', $temp).');';
			$result[] = $this->createMethod($method);
		}
		
		
		return join("\n\n\t", $result);
	}
	
	protected function paramModelConstants($data)
	{
		$columns = $data['table']->getColumnNames();
		$result = array();
		foreach ($columns as $column) {
			$result[] = "const " . strtoupper($column) . " = '$column';";
		}
		return join("\n\t", $result);
	}
	
	###
	
	protected function paramManagerColumns($data)
	{
		$columns = $data['table']->getColumns();
		return strtr(var_export($columns, true), array("\n    " => '', ",\n  )" => ')', "=> \n  array" => '=> array', "  '" => "\t\t\t\t'", "\n)" => "\n\t\t\t)"));
	}
	
	protected function paramManagerPrimary($data)
	{
		$primary = $data['table']->getPrimary();
		return  strtr(var_export($primary, true), array("\n" => '', '  ' => ''));
	}
	
	protected function paramManagerMethods($data)
	{
		$model = $data['name'];
		$metadata = $data['metadata'];
		$primary = $data['table']->getPrimary();
		$columns = $metadata['columns'];
		
		$result = array();
		if ($primary) {
		// get
			$method = array(
				'static' => true,
				'name' => 'get',
				'return' => array('type' => $model),
				'params' => array()
			);
			$temp = array();
			$temp2 = array();
			foreach ($primary as $column) {
				$fColumn = $this->columnToParam($column);
				$temp[] = '$' . $fColumn;
				$temp2[] = $fColumn;
				$method['params'][] = array(
					'name' => $fColumn,
					'type' => $this->dbTypeToPhpType($columns[$column]['type'], $columns[$column]['rtype'])
				);
			}
			$temp = (count($temp) > 1) ? 'array('.join(', ', $temp).')' : $temp[0];
			$method['body'] = "\tself::init();\n\t\t"
				. 'return self::$table->getOne('.$temp.');';
			$result[] = $this->createMethod($method);
		// fetch
			$desc = 'Array of ';
			if (count($primary) == 1) {
				$desc .= '' . $temp2[0] . '\'s';
			} else {
				$desc .= 'arrays as (' . join(', ', $temp2) . ') values';
			}
			$method = array(
				'static' => true,
				'name' => 'fetch',
				'return' => array('type' => 'array', 'desc' => 'Array of ' . $model . ' objects'),
				'params' => array(
					array('type' => 'array', 'name' => 'values', 'desc' => $desc),
					array('type' => 'mixed', 'name' => 'order', 'default' => 'null'),
					array('type' => 'int', 'name' => 'count', 'default' => 'null'),
					array('type' => 'offset', 'name' => 'offset', 'default' => 'null'),
				)
			);
			$method['body'] = "\tself::init();\n\t\t"
				. 'return self::$table->get($values, $order, $count, $offset);';
			$result[] = $this->createMethod($method);
		}
		
		// indexes
		$indexes = $metadata['indexes'];
		foreach ($indexes as $index) {
			$name = $this->formatName($index['columns']);
			if ($index['unique']) {
				$method = array(
					'static' => true,
					'name' => 'getBy' . $name,
					'return' => array('type' => $model),
					'params' => array()
				);
				$temp = array();
				foreach ($index['columns'] as $column) {
					$fColumn = $this->columnToParam($column);
					$temp[] = '\''.$column.'=?\' => $' . $fColumn;
					$method['params'][] = array(
						'name' => $fColumn,
						'type' => $this->dbTypeToPhpType($columns[$column]['type'], $columns[$column]['rtype'])
					);
				}
				$temp = 'array('.join(', ', $temp).')';
				$method['body'] = "\tself::init();\n\t\t"
					. 'return self::$table->fetchOne('.$temp.');';
				$result[] = $this->createMethod($method);
			} else {
				$unique = false;
				foreach ($index['columns'] as $column) {
					if ($columns[$column]['unique']) {
						$unique = true;
						break;
					}
				}
				
				$method = array(
					'static' => true,
					'name' => 'getBy' . $name,
					'return' => array('type' => 'array', 'desc' => 'Array of ' . $model . ' objects'),
					'params' => array()
				);
				$temp = array();
				foreach ($index['columns'] as $column) {
					$fColumn = $this->columnToParam($column);
					$temp[] = '\''.$column.'=?\' => $'.$fColumn;
					$method['params'][] = array(
						'name' => $fColumn,
						'type' => $this->dbTypeToPhpType($columns[$column]['type'], $columns[$column]['rtype'])
					);
				}
				$temp = 'array('.join(', ', $temp).')';
				if ($unique) {
					$method['body'] = "\tself::init();\n\t\t"
						. 'return self::$table->fetchOne('.$temp.');';
				} else {
					$method['params'] = array_merge($method['params'], array(
						array('type' => 'mixed', 'name' => 'order', 'default' => 'null'),
						array('type' => 'int', 'name' => 'count', 'default' => 'null'),
						array('type' => 'int', 'name' => 'offset', 'default' => 'null')
						));
					$method['body'] = "\tself::init();\n\t\t"
						. 'return self::$table->fetch('.$temp.', $order, $count, $offset);';
				}
				
				
				$result[] = $this->createMethod($method);
			
			}
		}
		
		return join("\n\n\t", $result);
	}
	
	###
	
	protected function dbTypeToPhpType($type, $rtype)
	{
		switch ($rtype) {
			case 'enum':
				return 'string';
			case 'date':
				if ($type == 'year') {
					return 'int';
				} else {
					return 'string';
				}
			default:
				return $rtype;
		}
	}
	
	protected function createMethod($data)
	{
		$result = array("/**");
		if (isset($data['title'])) {
			$result[] = ' * ' . $data['title'];
			$result[] = ' * ';
		}
		$params = array();
		if (isset($data['params'])) {
			foreach ($data['params'] as $param) {
				$params[] = '$' . $param['name'] . (isset($param['default']) ? ' = ' . $param['default'] : '');
				$result[] = ' * @param ' . $param['type'] . ' $' . $param['name'] . (isset($param['desc']) ? ' ' . $param['desc'] : '');
			}
		}
		if (isset($data['return'])) {
			$result[] = ' * @return ' . $data['return']['type'] . (isset($data['return']['desc']) ? ' ' . $data['return']['desc'] : '');
		}
		$result[] = ' */';
		$result[] = (isset($data['static']) ? 'static ' : '')
			. 'public function '
			. $data['name'] . '(' . join(', ', $params) . ')';
		$result[] = '{';
		$result[] = $data['body'];// str_replace("\n", "\t\t", $data['body']);
		$result[] = '}';
		
		return join("\n\t", $result);
	}
	
	###
	
	protected function columnToParam($columt)
	{
		return $this->formatName($columt, true);
	}
	
	protected function tableToModel($table)
	{
		$model = $this->formatName($table);
		$matches = array();
		if (preg_match('`(xes|ses|ies|(?<!s)s)$`', $model, $matches)) {
			switch ($matches[1]) {
				case 'xes':
				case 'ses':
					$model = substr($model, 0, -2);
					break;
				case 'ies':
					$model = substr($model, 0, -3) . 'y';
					break;
				case 's':
					$model = substr($model, 0, -1);
					break;
			}
		}
		return $model;
	}
	
	protected function formatName($string, $lowerFirstCar = false)
	{
		if (is_array($string)) {
			$array = $string;
			$string = array();
			foreach ($array as $str) {
				$string[] = $this->formatName($str, $lowerFirstCar);
			}
			return join('And', $string);
		}
		$string = ucwords(str_replace('_', ' ', $string));
		if ($lowerFirstCar) {
			$string = explode(' ', $string);
			$string[0] = strtolower($string[0]);
			$string = join('', $string);
		} else {
			$string = str_replace(' ', '', $string);
		}
		return $string;
	}
	
	###
	
	protected function printLine($text)
	{
		$this->printText($text . "\n");
	}
	
	protected function printText($text)
	{
		if (defined('STDOUT')) {
			fwrite(STDOUT, $text);
		} else {
			echo $text;
		}
	}
	
	protected $templates = array(
################################################################################
'manager-file' =>
'<?php
require_once \'%base_manager_file%\';

class %manager% extends %base_manager%
{}',
	
################################################################################
'model-file' =>
'<?php
require_once \'%base_model_file%\';

class %model% extends %base_model%
{}',
	
################################################################################
'base-manager-file' =>
'<?php
abstract class %base_manager%
{
	/**
	 * @var bool
	 */
	static protected $inited = false;
	
	/**
	 * @var Eka_Db_Table_Model
	 */
	static protected $table;

	static protected function init()
	{
		if (self::$inited) {
			return;
		}
		self::$inited = true;
		self::$table = new Eka_Db_Table_Model(
			%adapter%,
			\'%table%\', \'%schema%\', \'%model%\',
			%columns%,
			%primary%
		);
	}
	
	%methods%
	
	/**
	 * @param mixed $where
	 * @param mixed $order
	 * @param int $count
	 * @param int $offset
	 * @return array
	 */
	static public function select($where = null, $order = null, $count = null, $offset = null)
	{
		self::init();
		return self::$table->fetch($where, $order, $count, $offset);
	}
	
	/**
	 * @return Eka_Db_Table_Model
	 */
	static public function table()
	{
		self::init();
		return self::$table;
	}
}',

################################################################################
'base-model-file' =>
'<?php
require_once \'Eka/Db/Table/Row/Abstract.php\';

abstract class %base_model% extends Eka_Db_Table_Row_Model
{
	%constants%

	/**
	 * Constructor
	 *
	 * @param array $data
	 * @param bool $new
	 */
	public function __construct(array $data = null, $new = true)
	{
		parent::__construct(%manager%::table(), $data, $new);
	}
	
	%methods%
}',

	);
	
}